/**
 * Copyright 1998-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
 * All rights reserved.
 */
package ca.uvic.cs.cogz.mapping.ui.renderer;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;

import ca.uvic.cs.cogz.mapping.core.AbstractMapElement;
import ca.uvic.cs.cogz.mapping.core.MappingCollection;
import ca.uvic.cs.cogz.util.TreeUtil;
import edu.stanford.smi.protege.model.Frame;

/**
 * Class for rendering mapping arcs on the screen.
 * 
 * @author Chris Callendar, Sean Falconer
 * @date 14-Nov-06
 */
public class ArcRenderer {
	public static final int ELEMENTS_FOR_COMPOSITE = 5;

	private static final double CURVE_CONSTANT = 20.0;

	/** represents the y-coordinate for the top part of the arcs panel */
	public static final int TOP_PANEL_LOCATION = 50;

	private static final Point2D.Double CP1 = new Point2D.Double(0.5d, 0d); // first control point
	// for cubic curved arc
	private static final Point2D.Double CP2 = new Point2D.Double(0.5d, 1d); // second control point
	// for cubic curved arc

	/** reference to the MappingCollection object */
	private MappingCollection mappingCollection;

	/** collection that associates the drawn shape of a mapping with the actual mapping element */
	private HashMap<Shape, List<AbstractMapElement>> shapes;

	/**
	 * collection that associates the drawn composite shape of several mappings with all of the
	 * actual mapping element
	 */
	private HashMap<Shape, List<AbstractMapElement>> compositeShapes;

	/** object references for the source and target ontology trees */
	private JTree sourceTree;
	private JTree targetTree;

	private JPanel arcsPanel;

	private Point dragPoint = null; // in the arcs panel coordinates
	private JTree dragSource = null;

	private TreePath sourcePath;

	/** flag for indicating all arcs should be hidden, true for when suggestion list is visible */
	private boolean hideArcs;

	/** flag for indicating whether background arcs should be filtered */
	private boolean filterBackgroundArcs;

	/** flag for indicating that a mapped item is selected in the display */
	private boolean mappedItemSelected;

	private AbstractMapElement hoveredElement = null;

	private Set<Frame> expandedItems;

	public ArcRenderer(MappingCollection mappingCollection) {
		this.mappingCollection = mappingCollection;
		this.expandedItems = new HashSet<Frame>();

		this.arcsPanel = getArcsPanel();
		this.shapes = new HashMap<Shape, List<AbstractMapElement>>();
		this.compositeShapes = new HashMap<Shape, List<AbstractMapElement>>();
	}

	public void reset() {
		shapes.clear();
		compositeShapes.clear();
	}

	public void setFilterBackgroundArcs(boolean filterBackgroundArcs) {
		this.filterBackgroundArcs = filterBackgroundArcs;
	}

	public boolean getFilterBackgroundArcs() {
		return filterBackgroundArcs;
	}

	public void setHideArcs(boolean hideArcs) {
		if (hideArcs != this.hideArcs) {
			this.hideArcs = hideArcs;
			arcsPanel.repaint();
		}
	}

	public int getSize() {
		return shapes.size() + compositeShapes.size();
	}

	public void setSourcePath(TreePath sourcePath) {
		this.sourcePath = sourcePath;
	}

	public TreePath getSourcePath() {
		return sourcePath;
	}

	public void setHoveredElement(AbstractMapElement hoveredElement) {
		this.hoveredElement = hoveredElement;
	}

	public AbstractMapElement getHoveredElement() {
		return hoveredElement;
	}

	public void setSourceTree(JTree sourceTree) {
		this.sourceTree = sourceTree;
	}

	public void setTargetTree(JTree targetTree) {
		this.targetTree = targetTree;
	}

	public void setDragPoint(Point dragPoint) {
		this.dragPoint = dragPoint;
	}

	public JTree getDragSource() {
		return dragSource;
	}

	public void setDragSource(JTree dragSource) {
		this.dragSource = dragSource;
	}

	public void setMappedItemSelected(boolean mappedItemSelected) {
		this.mappedItemSelected = mappedItemSelected;
	}

	public boolean isMappedItemSelected() {
		return mappedItemSelected;
	}

	public boolean isExpanded(List<AbstractMapElement> mapElements) {
		if (mapElements != null && mapElements.size() > 0) {
			Frame sourceFrame = mapElements.get(0).getSourceFrame();
			return expandedItems.contains(sourceFrame);
		}

		return false;
	}

	public void addToExpansionList(List<AbstractMapElement> mapElements) {
		if (mapElements != null && mapElements.size() > 0) {
			Frame sourceFrame = mapElements.get(0).getSourceFrame();
			expandedItems.add(sourceFrame);
		}
	}

	public void removeFromExpansionList(List<AbstractMapElement> mapElements) {
		if (mapElements != null && mapElements.size() > 0) {
			Frame sourceFrame = mapElements.get(0).getSourceFrame();
			expandedItems.remove(sourceFrame);
		}
	}

	/**
	 * Gets the map element at the given point coordinates based on the map elements curve.
	 */
	public List<AbstractMapElement> getMapElement(Point p) {
		// make sure the coordinates are valid
		if (p.y >= 0 && p.x >= 0) {
			// check the composite shapes first
			/*for (Shape shape : compositeShapes.keySet()) {
				if (shape.contains(p)) {
					return compositeShapes.get(shape);
				}
			}*/
			for (Shape shape : shapes.keySet()) {
				if (shape.contains(p)) {
					return shapes.get(shape);
				}
			}
		}
		return null;
	}

	public JPanel getArcsPanel() {
		if (arcsPanel == null) {
			arcsPanel = new JPanel(null, true) {
				public void paint(Graphics g) {
					super.paintComponent(g);
					if (!hideArcs) {
						paintConnections(g);
					}
				}
			};
			arcsPanel.setDoubleBuffered(true);
			arcsPanel.setOpaque(false);
			arcsPanel.setLocation(0, TOP_PANEL_LOCATION);
		}
		return arcsPanel;
	}

	/**
	 * Draws the mapping lines between visible MapElements
	 */
	public void paintConnections(Graphics g) {
		// now paint the connections to make them on top of the children
		Graphics2D g2 = (Graphics2D) g;

		setRenderingHints(g2);

		if (mappingCollection.getSize() > 0) {
			shapes.clear();
			compositeShapes.clear();

			Map<Frame, List<AbstractMapElement>> sourceFrameMappings = mappingCollection.getSourceFrameMappings();
			for (Entry<Frame, List<AbstractMapElement>> entry : sourceFrameMappings.entrySet()) {
				for (AbstractMapElement mapElement : entry.getValue()) {

					TreePath sourcePath = TreeUtil.getTreePath(sourceTree, mapElement.getSourcePath());
					TreePath targetPath = TreeUtil.getTreePath(targetTree, mapElement.getTargetPath());

					if (isVisible(sourcePath, targetPath) && !mappingCollection.isFiltered(mapElement)) {
						//if (mappingCollection.isFiltered(mapElement)) {
						//	continue;
						//}

						// turn on the appropriate flags and highlighting
						if (mapElement.equals(hoveredElement)) {
							mapElement.setHightlight(true);
							mapElement.setSelected(false);
						
						} else if (isSelected(sourcePath, sourceTree) && isSelected(targetPath, targetTree)) {
							mapElement.setSelected(true);
							mapElement.setHightlight(false);
						} else {
							mapElement.setHightlight(false);
							mapElement.setSelected(false);
						}

						// filter this line if background filtering is enabled and this is not the selected or hovered map item
						if (mappedItemSelected && filterBackgroundArcs && !mapElement.isSelected() && !mapElement.equals(hoveredElement)) {
							continue;
						}

						//if (paintCompositeMapping(g2, sourcePath, entry.getKey(), entry.getValue())) {
						//	break;
						//}

						paintMapping(g2, mapElement, sourcePath, targetPath);
					}
				}
			}
		}

		// if dragging, render partial path
		if (dragPoint != null) {
			renderPartialPath(g2, dragSource, dragPoint);
		}
	}

	/**
	 * Checks to see if the mapping line is at all visible. It is only visible if the two paths
	 * exist and they fall within the visible tree rectangles.
	 */
	private boolean isVisible(TreePath sourcePath, TreePath targetPath) {
		if (sourcePath == null || targetPath == null) {
			return false;
		}

		Rectangle leftNodeRightEdge = sourceTree.getPathBounds(sourcePath);
		Rectangle rightNodeLeftEdge = targetTree.getPathBounds(targetPath);

		// do not draw this shape if the nodes are outside the view
		if (leftNodeRightEdge != null && rightNodeLeftEdge != null) {
			if (!sourceTree.getVisibleRect().contains(leftNodeRightEdge.getLocation()) && !targetTree.getVisibleRect().contains(rightNodeLeftEdge.getLocation())) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Sets up the canvas and rendering for the given map element.
	 */
	private void paintMapping(Graphics2D g2, AbstractMapElement mapElement, TreePath sourcePath, TreePath targetPath) {
		setCanvasPropertiesForMapElement(g2, mapElement);
		
		Shape shape = renderFullPath(g2, sourcePath, targetPath, mapElement.getLineThickness());
		if (shape != null) {
			// save the shape - it is needed to determine if the mouse is hovering over it
			// to display a tooltip
			List<AbstractMapElement> elements = new ArrayList<AbstractMapElement>();
			elements.add(mapElement);
			shapes.put(shape, elements);
		}
	}

	private void setCanvasPropertiesForMapElement(Graphics2D g2, AbstractMapElement mapElement) {
		if (!g2.getComposite().equals(mapElement.getComposite())) {
			g2.setComposite(mapElement.getComposite());
		}
		if (!g2.getStroke().equals(mapElement.getStroke())) {
			g2.setStroke(mapElement.getStroke());
		}
		if (!g2.getColor().equals(mapElement.getLineColor())) {
			g2.setColor(mapElement.getLineColor());
		}
	}

	private boolean paintCompositeMapping(Graphics2D g2, TreePath sourcePath, Frame sourceFrame, List<AbstractMapElement> mapElements) {
		Shape shape = null;
		if (mappingCollection.getNumOfMappings(sourceFrame) > ELEMENTS_FOR_COMPOSITE) {
			setupCanvas(g2, mapElements);
			shape = renderFilteredVersion(g2, sourcePath, sourceFrame, mapElements.size());
			if (shape != null) {
				// save the shape - it is needed to determine if the mouse is hovering over it
				// to display a tooltip
				compositeShapes.put(shape, mapElements);

				if (!expandedItems.contains(sourceFrame)) {
					return true;
				}
			}
		}

		return false;
	}

	private void setRenderingHints(Graphics2D g2) {
		// set rendering hints
		g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
	}

	private void setupCanvas(Graphics2D g2, List<AbstractMapElement> mapElements) {
		boolean selected = false;
		for (AbstractMapElement mapElement : mapElements) {
			TreePath sourcePath = TreeUtil.getTreePath(sourceTree, mapElement.getSourcePath());
			TreePath targetPath = TreeUtil.getTreePath(targetTree, mapElement.getTargetPath());

			// turn on the appropriate flags and highlighting
			if (mapElement.equals(hoveredElement)) {
				mapElement.setHightlight(true);
				mapElement.setSelected(false);
				selected = true;
			} else if (isSelected(sourcePath, sourceTree) && isSelected(targetPath, targetTree)) {
				mapElement.setSelected(true);
				mapElement.setHightlight(false);
				selected = true;
			} else {
				mapElement.setHightlight(false);
				mapElement.setSelected(false);
			}

			setCanvasPropertiesForMapElement(g2, mapElement);

			if (selected) {
				break;
			}
		}
	}

	/**
	 * Renders a partial mapping path. Used when the user is dragging a mapping between the two
	 * trees.
	 */
	public void renderPartialPath(Graphics2D g2, JTree tree, Point endPoint) {
		Rectangle treeBounds = tree.getBounds();
		Rectangle nodeBounds = tree.getPathBounds(tree.getSelectionPath());

		GeneralPath path;
		if (sourceTree.equals(tree)) {
			Point leftNodeRightEdge = new Point(nodeBounds.x + nodeBounds.width + 2, nodeBounds.y + (nodeBounds.height / 2));
			Point leftTreeRightEdge = new Point(treeBounds.x + treeBounds.width, nodeBounds.y + (nodeBounds.height / 2));
			leftNodeRightEdge = SwingUtilities.convertPoint(tree, leftNodeRightEdge, getArcsPanel());
			leftTreeRightEdge = SwingUtilities.convertPoint(tree, leftTreeRightEdge, getArcsPanel());
			path = createPathFromLeft(leftNodeRightEdge, leftTreeRightEdge, endPoint);
		} else {
			Point rightNodeLeftEdge = new Point(nodeBounds.x - 2, nodeBounds.y + (nodeBounds.height / 2));
			Point rightTreeLeftEdge = new Point(treeBounds.x, nodeBounds.y + (nodeBounds.height / 2));
			rightNodeLeftEdge = SwingUtilities.convertPoint(tree, rightNodeLeftEdge, getArcsPanel());
			rightTreeLeftEdge = SwingUtilities.convertPoint(tree, rightTreeLeftEdge, getArcsPanel());
			path = createPathFromRight(rightNodeLeftEdge, rightTreeLeftEdge, endPoint);
		}

		g2.setComposite(AbstractMapElement.ALPHA_PARTIAL_TRANSPARENT);
		g2.setStroke(new BasicStroke(AbstractMapElement.DEFAULT_LINE_THICKNESS));
		g2.setColor(AbstractMapElement.DEFAULT_LINE_COLOR);
		g2.draw(path);
	}

	private boolean isSelected(TreePath path, JTree tree) {
		return TreeUtil.pathContains(tree.getSelectionPaths(), path);
	}

	private Shape renderFilteredVersion(Graphics2D g2, TreePath left, Frame sourceFrame, int numOfMappings) {
		Point leftNodeRightEdge = getLeftNodeRightEdge(left);
		Point leftNodeTreeEdge = getLeftNodeTreeEdge(left);

		GeneralPath path = getSourceSectionOfPath(left, leftNodeRightEdge, leftNodeTreeEdge);
		if (path == null) {
			return null;
		}

		int height = sourceTree.getParent().getHeight();

		// make sure the node's tree edge is visible
		if (leftNodeTreeEdge.y < 0 || leftNodeTreeEdge.y > height) {
			return null;
		}

		g2.draw(path);
		Stroke prevStroke = g2.getStroke();
		g2.setStroke(new BasicStroke(1.0f));

		// figure out the area of the triangle based on the number of relative mappings for this frame
		double factor = 18 / Math.log(mappingCollection.getLargestNumberOfMappings() + 1);
		float area = (float) (Math.log(numOfMappings + 1) * factor) + 10;

		// create a triangle representation of the mapping items
		GeneralPath triangle = new GeneralPath();
		triangle.moveTo(leftNodeTreeEdge.x + 10, leftNodeTreeEdge.y);
		triangle.lineTo(leftNodeTreeEdge.x + 10, leftNodeTreeEdge.y - area / 2);
		triangle.lineTo(leftNodeTreeEdge.x + area, leftNodeTreeEdge.y);
		triangle.lineTo(leftNodeTreeEdge.x + 10, leftNodeTreeEdge.y + area / 2);
		triangle.lineTo(leftNodeTreeEdge.x + 10, leftNodeTreeEdge.y);

		g2.fill(triangle);

		Color previousColor = g2.getColor();
		g2.setColor(Color.black);

		// draw the open/closed indicator on the triangle
		if (expandedItems.contains(sourceFrame)) {
			g2.drawString("-", leftNodeTreeEdge.x + area / 2 + 5, leftNodeTreeEdge.y - area / 2 + 5);
		} else {
			g2.drawString("+", leftNodeTreeEdge.x + area / 2 + 5, leftNodeTreeEdge.y - area / 2 + 5);
		}

		// restore the color and stroke for further drawing
		g2.setColor(previousColor);
		g2.setStroke(prevStroke);

		return triangle;
	}

	private GeneralPath createPathFromLeft(Point leftNodeRightEdge, Point leftTreeRightEdge, Point endPoint) {
		GeneralPath path = new GeneralPath();
		path.moveTo(leftNodeRightEdge.x, leftNodeRightEdge.y);
		if (endPoint.x > leftTreeRightEdge.x) {
			path.lineTo(leftTreeRightEdge.x, leftTreeRightEdge.y);
			path.append(createCurve(leftTreeRightEdge, endPoint), false);
		} else {
			path.lineTo(endPoint.x, leftTreeRightEdge.y);
		}
		return path;
	}

	private GeneralPath createPathFromRight(Point rightNodeLeftEdge, Point rightTreeLeftEdge, Point endPoint) {
		GeneralPath path = new GeneralPath();
		path.moveTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y);
		if (endPoint.x < rightTreeLeftEdge.x) {
			path.lineTo(rightTreeLeftEdge.x, rightTreeLeftEdge.y);
			path.append(createCurve(endPoint, rightTreeLeftEdge), false);
		} else {
			path.lineTo(endPoint.x, rightTreeLeftEdge.y);
		}
		return path;
	}

	/**
	 * Determines if the left node and right node are both above or both below the tree view.
	 * 
	 * @return True if both nodes are above or both are below the view.
	 */
	private boolean outOfBounds(Point leftNodeBounds, Point rightNodeBounds, int height) {
		return (leftNodeBounds.y < 0 && rightNodeBounds.y < 0) || (leftNodeBounds.y >= height && rightNodeBounds.y >= height);
	}

	private Point getLeftNodeTreeEdge(TreePath left) {
		// get the bounds for the trees and paths
		Rectangle sourceTreeBounds = sourceTree.getVisibleRect();
		Rectangle leftNodeBounds = sourceTree.getPathBounds(left);

		if (leftNodeBounds == null) {
			return null;
		}

		int y1 = leftNodeBounds.y + (leftNodeBounds.height / 2);

		Point leftNodeTreeEdge = new Point(sourceTreeBounds.x + sourceTreeBounds.width, y1);
		leftNodeTreeEdge = SwingUtilities.convertPoint(sourceTree, leftNodeTreeEdge, getArcsPanel());

		return leftNodeTreeEdge;
	}

	private Point getLeftNodeRightEdge(TreePath left) {
		// get the bounds for the trees and paths
		Rectangle sourceTreeBounds = sourceTree.getVisibleRect();
		Rectangle leftNodeBounds = sourceTree.getPathBounds(left);

		if (leftNodeBounds == null) {
			return null;
		}

		int y1 = leftNodeBounds.y + (leftNodeBounds.height / 2);

		Point leftNodeRightEdge = new Point(leftNodeBounds.x + leftNodeBounds.width, y1);
		Point leftNodeTreeEdge = new Point(sourceTreeBounds.x + sourceTreeBounds.width, y1);

		if (leftNodeRightEdge.x > leftNodeTreeEdge.x) {
			leftNodeRightEdge.setLocation(leftNodeTreeEdge.x, leftNodeRightEdge.y);
		}

		leftNodeRightEdge = SwingUtilities.convertPoint(sourceTree, leftNodeRightEdge, getArcsPanel());
		leftNodeTreeEdge = SwingUtilities.convertPoint(sourceTree, leftNodeTreeEdge, getArcsPanel());

		return leftNodeRightEdge;
	}

	private GeneralPath getSourceSectionOfPath(TreePath left, Point leftNodeRightEdge, Point leftNodeTreeEdge) {
		if (leftNodeRightEdge == null || leftNodeTreeEdge == null) {
			return null;
		}

		// get the height of the container of the tree, used to determine if tree lines should be
		// drawn
		int height = sourceTree.getParent().getHeight();

		// this is the path that is drawn
		GeneralPath path = new GeneralPath();
		path.moveTo(leftNodeRightEdge.x, leftNodeRightEdge.y);
		if (leftNodeRightEdge.y < height) {
			path.lineTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y);
		} else {
			path.moveTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y);
		}

		return path;
	}

	/**
	 * Draws the mapping curve between the source and target tree paths. This method also returns a
	 * shape used for detecting collisions with a mapping arc. The returned shape is larger than the
	 * rendered shape in order to make the arc easier to interact with.
	 */
	private Shape renderFullPath(Graphics2D g2, TreePath left, TreePath right, float thickness) {
		try {
			// get the bounds for the trees and paths
			Rectangle targetTreeBounds = targetTree.getBounds();
			Rectangle rightNodeBounds = targetTree.getPathBounds(right);

			int y2 = rightNodeBounds.y + (rightNodeBounds.height / 2);

			Point leftNodeRightEdge = getLeftNodeRightEdge(left);
			Point leftNodeTreeEdge = getLeftNodeTreeEdge(left);
			Point rightNodeTreeEdge = new Point(targetTreeBounds.x, y2);
			Point rightNodeLeftEdge = new Point(rightNodeBounds.x, y2);

			if (leftNodeRightEdge.x > leftNodeTreeEdge.x) {
				leftNodeRightEdge.setLocation(leftNodeTreeEdge.x, leftNodeRightEdge.y);
			}

			rightNodeLeftEdge = SwingUtilities.convertPoint(targetTree, rightNodeLeftEdge, arcsPanel);
			rightNodeTreeEdge = SwingUtilities.convertPoint(targetTree, rightNodeTreeEdge, arcsPanel);

			// get the height of the container of the tree, used to determine if tree lines should be
			// drawn
			int height = sourceTree.getParent().getHeight();

			// do not draw this shape if the nodes are outside the view
			if (outOfBounds(leftNodeRightEdge, rightNodeLeftEdge, height)) {
				return null;
			}

			Point2D.Float[] ctrlPoints = getCurveControlPoints(leftNodeTreeEdge, rightNodeTreeEdge);
			Point2D.Float cp1 = ctrlPoints[0];
			Point2D.Float cp2 = ctrlPoints[1];

			if (leftNodeRightEdge == null || leftNodeTreeEdge == null) {
				return null;
			}

			// this is the path that is drawn
			GeneralPath path = getSourceSectionOfPath(left, leftNodeRightEdge, leftNodeTreeEdge);

			path.curveTo(cp1.x, cp1.y, cp2.x, cp2.y, rightNodeTreeEdge.x, rightNodeTreeEdge.y);
			if (rightNodeLeftEdge.y < height) {
				path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y);
			}

			g2.draw(path);

			final float shift = (thickness + 2) / 2;
			// this is the path used for checking containment of a point
			path = new GeneralPath();

			// start the path from the right edge of the left node
			path.moveTo(leftNodeRightEdge.x, leftNodeRightEdge.y - shift);
			path.lineTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y - shift);
			path.curveTo(cp1.x, cp1.y - shift, cp2.x, cp2.y - shift, rightNodeTreeEdge.x, rightNodeTreeEdge.y - shift);
			path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y - shift);

			// close the path going in the opposite direction
			path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y + shift);
			path.lineTo(rightNodeTreeEdge.x, rightNodeTreeEdge.y + shift);
			path.curveTo(cp2.x, cp2.y + shift, cp1.x, cp1.y + shift, leftNodeTreeEdge.x, leftNodeTreeEdge.y + shift);
			path.lineTo(leftNodeRightEdge.x, leftNodeRightEdge.y + shift);
			path.lineTo(leftNodeRightEdge.x, leftNodeRightEdge.y - shift);
			path.closePath();

			return path;
		} catch (NullPointerException e) {
			return null;
		}
	}

	/**
	 * Draws the mapping curve between the source and target tree paths. This method also returns a
	 * shape used for detecting collisions with a mapping arc. The returned shape is larger than the
	 * rendered shape in order to make the arc easier to interact with.
	 */
	private Shape renderFullPathAsLine(Graphics2D g2, TreePath left, TreePath right, float thickness) {
		try {
			// get the bounds for the trees and paths
			Rectangle targetTreeBounds = targetTree.getBounds();
			Rectangle rightNodeBounds = targetTree.getPathBounds(right);

			int y2 = rightNodeBounds.y + (rightNodeBounds.height / 2);

			Point leftNodeRightEdge = getLeftNodeRightEdge(left);
			Point leftNodeTreeEdge = getLeftNodeTreeEdge(left);
			Point rightNodeTreeEdge = new Point(targetTreeBounds.x, y2);
			Point rightNodeLeftEdge = new Point(rightNodeBounds.x, y2);

			if (leftNodeRightEdge.x > leftNodeTreeEdge.x) {
				leftNodeRightEdge.setLocation(leftNodeTreeEdge.x, leftNodeRightEdge.y);
			}

			rightNodeLeftEdge = SwingUtilities.convertPoint(targetTree, rightNodeLeftEdge, arcsPanel);
			rightNodeTreeEdge = SwingUtilities.convertPoint(targetTree, rightNodeTreeEdge, arcsPanel);

			// get the height of the container of the tree, used to determine if tree lines should be
			// drawn
			int height = sourceTree.getParent().getHeight();

			// do not draw this shape if the nodes are outside the view
			if (outOfBounds(leftNodeRightEdge, rightNodeLeftEdge, height)) {
				return null;
			}

			//Point2D.Float[] ctrlPoints = getCurveControlPoints(leftNodeTreeEdge, rightNodeTreeEdge);
			//Point2D.Float cp1 = ctrlPoints[0];
			//Point2D.Float cp2 = ctrlPoints[1];

			if (leftNodeRightEdge == null || leftNodeTreeEdge == null) {
				return null;
			}

			// get the height of the container of the tree, used to determine if tree lines should be
			// drawn

			// this is the path that is drawn
			//path.moveTo(leftNodeRightEdge.x, leftNodeRightEdge.y);
			if (leftNodeRightEdge.y < height) {
				Line2D line = new Line2D.Double(leftNodeRightEdge.x, leftNodeRightEdge.y, leftNodeTreeEdge.x, leftNodeTreeEdge.y);
				g2.draw(line);
			}

			Line2D line = new Line2D.Double(leftNodeTreeEdge.x, leftNodeTreeEdge.y, rightNodeTreeEdge.x, rightNodeTreeEdge.y);
			g2.draw(line);

			Line2D line2 = new Line2D.Double(rightNodeTreeEdge.x, rightNodeTreeEdge.y, rightNodeLeftEdge.x, rightNodeLeftEdge.y);
			g2.draw(line2);

			final float shift = (thickness + 2) / 2;

			GeneralPath path = new GeneralPath();
			path.moveTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y - shift);
			path.lineTo(rightNodeTreeEdge.x, rightNodeTreeEdge.y - shift);
			path.lineTo(rightNodeTreeEdge.x, rightNodeTreeEdge.y + shift);
			path.lineTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y + shift);
			path.closePath();

			//g2.draw(path);

			return path;

			// this is the path that is drawn
//			GeneralPath path = getSourceSectionOfPath(left, leftNodeRightEdge, leftNodeTreeEdge);
//
//			//path.curveTo(cp1.x, cp1.y, cp2.x, cp2.y, rightNodeTreeEdge.x, rightNodeTreeEdge.y);
//			path.lineTo(rightNodeTreeEdge.x, rightNodeTreeEdge.y);
//			if (rightNodeLeftEdge.y < height) {
//				path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y);
//			}
//
//			g2.draw(path);
//
//			//if (true) {
//			return path;
			//}

//			final float shift = (thickness + 2) / 2;
//			// this is the path used for checking containment of a point
//			path = new GeneralPath();
//
//			// start the path from the right edge of the left node
//			path.moveTo(leftNodeRightEdge.x, leftNodeRightEdge.y - shift);
//			path.lineTo(leftNodeTreeEdge.x, leftNodeTreeEdge.y - shift);
//			path.curveTo(cp1.x, cp1.y - shift, cp2.x, cp2.y - shift, rightNodeTreeEdge.x, rightNodeTreeEdge.y - shift);
//			path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y - shift);
//
//			// close the path going in the opposite direction
//			path.lineTo(rightNodeLeftEdge.x, rightNodeLeftEdge.y + shift);
//			path.lineTo(rightNodeTreeEdge.x, rightNodeTreeEdge.y + shift);
//			path.curveTo(cp2.x, cp2.y + shift, cp1.x, cp1.y + shift, leftNodeTreeEdge.x, leftNodeTreeEdge.y + shift);
//			path.lineTo(leftNodeRightEdge.x, leftNodeRightEdge.y + shift);
//			path.lineTo(leftNodeRightEdge.x, leftNodeRightEdge.y - shift);
//			path.closePath();
//
//			return path;
		} catch (NullPointerException e) {
			return null;
		}
	}

	/**
	 * Draws an arrow head (pointing right).
	 * 
	 * @param leftMidPoint vertical middle point on the left side.
	 * @param width the width of the arrow, it's height will be half this value
	 */
	@SuppressWarnings("unused")
	private void drawArrowHead(Graphics2D g2, Point leftMidPoint, float width) {
		float halfHeight = width / 2;
		GeneralPath arrowPath = new GeneralPath();
		arrowPath.moveTo(leftMidPoint.x, leftMidPoint.y); // the end point
		arrowPath.lineTo(leftMidPoint.x - width, leftMidPoint.y + halfHeight);
		arrowPath.lineTo(leftMidPoint.x - width, leftMidPoint.y - halfHeight);
		arrowPath.lineTo(leftMidPoint.x, leftMidPoint.y);
		g2.fill(arrowPath);
	}

	/**
	 * Creates the two control points needed for a cubic curve.
	 */
	private Point2D.Float[] getCurveControlPoints(Point srcPoint, Point destPoint) {
		double d = 0 * CURVE_CONSTANT;
		double dx = destPoint.getX() - srcPoint.getX();
		double dy = destPoint.getY() - srcPoint.getY();
		Point2D.Float ctrlPoint1 = new Point2D.Float((float) (srcPoint.getX() + dx * CP1.getX() - d), (float) (srcPoint.getY() + dy * CP1.getY() - d));
		Point2D.Float ctrlPoint2 = new Point2D.Float((float) (srcPoint.getX() + dx * CP2.getX() + d), (float) (srcPoint.getY() + dy * CP2.getY() + d));
		return new Point2D.Float[] { ctrlPoint1, ctrlPoint2 };
	}

	/**
	 * Creates the {@link CubicCurve2D} between the src and destination points.
	 */
	private CubicCurve2D createCurve(Point srcPoint, Point destPoint) {
		Point2D.Float[] ctrlPoints = getCurveControlPoints(srcPoint, destPoint);
		CubicCurve2D.Float curve = new CubicCurve2D.Float();
		curve.setCurve(srcPoint, ctrlPoints[0], ctrlPoints[1], destPoint);

		return curve;
	}

	protected GeneralPath createCompositeLinePath(Point srcPoint, Point destPoint) {
		int curveFactor = 4;
		double mag = 1.0;
		double weight = 4.0;

		final double CURVE_CONSTANT = 20;
		final double MIN_ARROW_HEAD_WIDTH = 20.0;
		final double MIN_ARROW_HEAD_LENGTH = 20.0;
		final double ARROW_HEAD_LENGTH_TO_ARC_WEIGHT_RATIO = 1.5;
		final double ARROW_HEAD_WIDTH_TO_ARC_WEIGHT_RATIO = 1.5;

		double dx = destPoint.x - srcPoint.x;
		double dy = destPoint.y - srcPoint.y;
		double d = Point2D.Double.distance(destPoint.x, destPoint.y, srcPoint.x, srcPoint.y);

		double slope = dy / dx;
		double theta = Math.atan(-(1.0 / slope));

		GeneralPath arrowPath = new GeneralPath();
		double lineWidth = weight / mag;
		double arrowHeadLength = Math.max(MIN_ARROW_HEAD_LENGTH / mag, lineWidth * ARROW_HEAD_LENGTH_TO_ARC_WEIGHT_RATIO);
		double arrowHeadWidth = Math.max(MIN_ARROW_HEAD_WIDTH / mag, lineWidth * ARROW_HEAD_WIDTH_TO_ARC_WEIGHT_RATIO);
		double rotate = (dy < 0) ? theta + 3.0 * Math.PI / 2.0 : theta + Math.PI / 2.0;
		AffineTransform rotateT = AffineTransform.getRotateInstance(rotate, srcPoint.x, srcPoint.y);
		AffineTransform t = new AffineTransform();
		t.concatenate(rotateT);

		if (curveFactor == 0) {
			Point2D.Double[] pts = new Point2D.Double[] { new Point2D.Double(srcPoint.x, srcPoint.y), new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y - lineWidth / 2.0),
					new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y - arrowHeadWidth / 2.0), new Point2D.Double(srcPoint.x + d, srcPoint.y),
					new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y + arrowHeadWidth / 2.0), new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y + lineWidth / 2.0) };
			for (int i = 0; i < pts.length; i++) {
				Point2D.Double pt = pts[i];
				if (i == 0) {
					arrowPath.moveTo((float) pt.x, (float) pt.y);
				} else {
					arrowPath.lineTo((float) pt.x, (float) pt.y);
				}
			}
			arrowPath.closePath();

		} else {

			double ctrlOffset = (CURVE_CONSTANT * curveFactor) / mag;

			Point2D.Double ctrlPoint = new Point2D.Double(srcPoint.x + d / 2.0, srcPoint.y + ctrlOffset);
			Point2D.Double p0 = new Point2D.Double(srcPoint.x, srcPoint.y);
			Point2D.Double p1 = new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y - lineWidth / 2.0);
			Point2D.Double p2 = new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y - arrowHeadWidth / 2.0);
			Point2D.Double p3 = new Point2D.Double(srcPoint.x + d, srcPoint.y); // tip of the arrow
			Point2D.Double p4 = new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y + arrowHeadWidth / 2.0);
			Point2D.Double p5 = new Point2D.Double(srcPoint.x + d - arrowHeadLength, srcPoint.y + lineWidth / 2.0);

			arrowPath.moveTo((float) p0.x, (float) p0.y);
			arrowPath.append(new QuadCurve2D.Double((float) p0.x, (float) p0.y, (float) ctrlPoint.x, (float) ctrlPoint.y, (float) p1.x, (float) p1.y), true);
			arrowPath.lineTo((float) p2.x, (float) p2.y);
			arrowPath.lineTo((float) p3.x, (float) p3.y);
			arrowPath.lineTo((float) p4.x, (float) p4.y);
			arrowPath.lineTo((float) p5.x, (float) p5.y);
			arrowPath.append(new QuadCurve2D.Double((float) p5.x, (float) p5.y, (float) ctrlPoint.x, (float) ctrlPoint.y, (float) p0.x, (float) p0.y), true);
			arrowPath.closePath();
		}
		return (GeneralPath) t.createTransformedShape(arrowPath);
	}
}
